﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;

namespace BusStopDataAccess
{
    /// <summary>
    /// A catch all collection of queries which are common to bus schedules for easier reuse
    /// </summary>
    public class CommonQueries
    {
        private const int MAX_RESULTS = 100;
        private const int MAX_RANGE_KM = 10;

        /// <summary>
        /// Returns a list of bus stops near a specified point, sorted by distance from that point ascending
        /// </summary>
        /// <param name="db">The database to look in</param>
        /// <param name="lat">Lattitude of the point</param>
        /// <param name="lon">Longitude of the point</param>
        /// <param name="rangeInKm">Maximum range from the point to look (in km) [0-10]</param>
        /// <param name="maxResults">Maximum number of results to return [1 - 100]</param>
        /// <returns>A list of bus stops near a specified point, sorted by distance from that point ascending</returns>
        public static IEnumerable<BusStops> StopsNearPoint(IScheduleInfo db, decimal lat, decimal lon, double rangeInKm, int maxResults)
        {
            Contract.Requires(db != null);
            Contract.Requires(rangeInKm >= 0);
            Contract.Requires(maxResults >= 1);

            int max = Math.Min(maxResults, MAX_RESULTS);
            double range = Math.Max(0, Math.Min(MAX_RANGE_KM, rangeInKm));

            Point origin = new Point() { Lat = lat, Lon = lon };
            decimal north = MapUtil.Project(origin, 0, range).Lat;
            decimal south = MapUtil.Project(origin, 180, range).Lat;
            decimal east = MapUtil.Project(origin, 90, range).Lon;
            decimal west = MapUtil.Project(origin, 270, range).Lon;

            var stops = from stop in db.BusStops
                        where
                        stop.Latitude > south && stop.Latitude < north &&
                        stop.Longitude > west && stop.Longitude < east
                        select stop;
            var stopsList = stops.ToList();
            stopsList.Sort(new DistanceComparer(origin));
            return stopsList.Take(max);
        }

        /// <summary>
        /// Returns the stop with the specified code or null if not found
        /// </summary>
        /// <param name="db">The database to look in</param>
        /// <param name="code">The bus stop code to look up</param>
        /// <returns>The stop with the specified code or null if not found</returns>
        public static IQueryable<BusStops> StopByCode(IScheduleInfo db, String code)
        {
            Contract.Requires(db != null);
            Contract.Requires(code != null);

            var stops = from stop in db.BusStops
                        where stop.Code == code
                        select stop;
            return stops;
        }

        /// <summary>
        /// Returns a list of routes serving the specified stop
        /// </summary>
        /// <param name="db"></param>
        /// <param name="stop"></param>
        /// <returns></returns>
        public static IQueryable<Routes> RoutesServingStop(IScheduleInfo db, BusStops stop)
        {
            Contract.Requires(db != null);
            Contract.Requires(stop != null);            

            if (stop == null) { return null; }
            return (from routestop in db.RouteStops
                          where routestop.BusStopID == stop.ID
                          select routestop.Routes);
        }

        private static readonly TimeSpan oneDay = TimeSpan.FromDays(1);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="db">The database to look in</param>
        /// <param name="stop"></param>
        /// <param name="timerange"></param>
        /// <returns></returns>
        public static Dictionary<Routes, IEnumerable<Departures>> NextTrips(IScheduleInfo db, BusStops stop, TimeSpan timerange)
        {
            Contract.Requires(db != null);
            Contract.Requires(stop != null);            
            Contract.Requires(timerange >= TimeSpan.Zero);

            TimeSpan start = DateTime.Now.Subtract(DateTime.Today);
            TimeSpan end = start.Add(timerange);

            return NextTrips(db, stop, DateTime.Today, start, end);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="db">The database to look in</param>
        /// <param name="stop"></param>
        /// <param name="date"></param>
        /// <param name="startTime"></param>
        /// <param name="endTime"></param>
        /// <returns></returns>
        public static Dictionary<Routes, IEnumerable<Departures>> NextTrips(IScheduleInfo db, BusStops stop, DateTime date, TimeSpan startTime, TimeSpan endTime)
        {
            Contract.Requires(db != null);
            Contract.Requires(stop != null);
            Contract.Requires(startTime <= endTime);

            IQueryable<Routes> routes = RoutesServingStop(db, stop);
            IQueryable<Service> serviceToday = LookupServices(db, date);
            IQueryable<Service> serviceYesterday = LookupServices(db, date.Subtract(oneDay));
            Dictionary<Routes, IEnumerable<Departures>> fromToday = NextTrips(db, stop, routes, serviceToday, startTime, endTime);
            Dictionary<Routes, IEnumerable<Departures>> fromYesterday = NextTrips(db, stop, routes, serviceYesterday, startTime.Add(oneDay), endTime.Add(oneDay));

            Dictionary<Routes, IEnumerable<Departures>> results = fromToday;
            foreach (Routes route in fromYesterday.Keys)
            {
                if (results.ContainsKey(route))
                { results[route] = fromToday[route].Concat(fromYesterday[route]); }
                else
                { results.Add(route, fromYesterday[route]); }
            }
            return results;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="db">The database to look in</param>
        /// <param name="stop"></param>
        /// <param name="routes"></param>
        /// <param name="service"></param>
        /// <param name="start"></param>
        /// <param name="end"></param>
        /// <returns></returns>
        public static Dictionary<Routes, IEnumerable<Departures>> NextTrips(IScheduleInfo db, BusStops stop, IQueryable<Routes> routes ,IQueryable<Service> service, TimeSpan start, TimeSpan end)
        {
            Contract.Requires(db != null);
            Contract.Requires(stop != null);
            Contract.Requires(routes != null);
            Contract.Requires(service != null);
            Contract.Requires(start <= end);

            int startMinute = ConvertTime(start);
            int endMinute = ConvertTime(end);

            var routeList = from r in routes
                            select r.ID;
            var serviceList = from s in service
                              select s.ID;
            var departures = (from departure in db.Departures
                              where departure.BusStopID == stop.ID &&
                              routeList.Contains(departure.RouteID) &&
                              serviceList.Contains(departure.ServiceID) &&
                              departure.Time >= startMinute &&
                              departure.Time <= endMinute
                              select departure).ToList().OrderBy(d => d.Time);
            Dictionary<Routes, IEnumerable<Departures>> results = new Dictionary<Routes, IEnumerable<Departures>>();

            foreach (Departures d in departures)
            {
                if (!results.ContainsKey(d.Routes))
                { results.Add(d.Routes, new List<Departures>()); }
                List<Departures> list = (List<Departures>)results[d.Routes];
                AddIfUnique(list, d);
            }
            return results;
        }

        /// <summary>
        /// it is possible for schedules to overlap.  If they overlap perfectly then this looks like duplicate data
        /// since the user doesn't care about these cases we supress the "duplicates"
        /// </summary>
        /// <param name="list">The list of departures to add to</param>
        /// <param name="d">The departure to be added</param>
        private static void AddIfUnique(IList<Departures> list, Departures d)
        {
            Contract.Requires(list != null);
            Contract.Requires(d != null);
            int time = d.Time;
            if (list.Where(d2 => d2.Time == time).FirstOrDefault() == null)
            { list.Add(d); }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="db">The database to look in</param>
        /// <param name="stop"></param>
        /// <param name="route"></param>
        /// <param name="service"></param>
        /// <param name="start"></param>
        /// <param name="end"></param>
        /// <returns></returns>
        public static IEnumerable <Departures> NextTrips(IScheduleInfo db, BusStops stop, Routes route, IQueryable<Service> service, TimeSpan start, TimeSpan end)
        {
            Contract.Requires(db != null);
            Contract.Requires(stop != null);
            Contract.Requires(route != null);
            Contract.Requires(service != null);
            Contract.Requires(start <= end);           

            int startMinute = ConvertTime(start);
            int endMinute = ConvertTime(end);

            var serviceList = from s in service
                               select s.ID;
            var departures = (from departure in db.Departures
                              where departure.BusStopID == stop.ID &&
                              departure.RouteID == route.ID &&
                              serviceList.Contains(departure.ServiceID) &&
                              departure.Time >= startMinute &&
                              departure.Time <= endMinute
                              select departure).ToList().OrderBy(d => d.Time);
                              
            return departures;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="db">The database to look in</param>
        /// <param name="date"></param>
        /// <returns></returns>
        public static IQueryable<Service> LookupServices(IScheduleInfo db, DateTime date)
        {
            Contract.Requires(db != null);

            DateTime dateComponent = date.Date;
            return (from sd in db.ServiceDates
                    where sd.Date == dateComponent
                    select sd.Service);
        }

        /// <summary>
        /// Converts between the .Net DateTime object and the internal time format used in the database
        /// </summary>
        /// <param name="time">The datetime object</param>
        /// <param name="NextDay">True if this is an overlap time (buses running in the early morning but counting as the previous days service)</param>
        /// <returns>An integer representing the number of minutes elapsed since midnight of the first day</returns>
        private static int ConvertTime(DateTime time, bool NextDay)
        {
            TimeSpan ts = time.Subtract(time.Date);
            if (NextDay) { ts.Add(TimeSpan.FromDays(1)); }
            return ConvertTime(ts);
        }

        /// <summary>
        /// Converts between the .Net DateTime object and the internal time format used in the database
        /// </summary>
        /// <param name="time">A .Net TimeSpan object</param>
        /// <returns>An integer representing the number of minutes elapsed since midnight of the first day</returns>
        private static int ConvertTime(TimeSpan time)
        { return (int) time.TotalMinutes; }

        /// <summary>
        /// Converts between the .Net DateTime object and the internal time format used in the database
        /// </summary>
        /// <param name="time">An integer representing the number of minutes elapsed since midnight of the first day</param>
        /// <returns>A .Net Timespan object representing the equivalent amount of time</returns>
        private static TimeSpan ConvertTime(int time)
        { return TimeSpan.FromMinutes(time); }
    }
}
